[slug].tsx 27 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678
  1. // @ts-nocheck
  2. import { zodResolver } from '@hookform/resolvers/zod'
  3. import { PermissionAction } from '@supabase/shared-types/out/constants'
  4. import { LOCAL_STORAGE_KEYS, useFlag, useParams } from 'common'
  5. import Head from 'next/head'
  6. import Link from 'next/link'
  7. import { useRouter } from 'next/router'
  8. import { PropsWithChildren, useEffect, useMemo, useState } from 'react'
  9. import { useForm } from 'react-hook-form'
  10. import { AWS_REGIONS, type CloudProvider } from 'shared-data'
  11. import { toast } from 'sonner'
  12. import { Button, Form, useWatch } from 'ui'
  13. import { Admonition } from 'ui-patterns/admonition'
  14. import ConfirmationModal from 'ui-patterns/Dialogs/ConfirmationModal'
  15. import { z } from 'zod'
  16. import { AUTO_ENABLE_RLS_EVENT_TRIGGER_SQL } from '@/components/interfaces/Database/Triggers/EventTriggersList/EventTriggers.constants'
  17. import { AdvancedConfiguration } from '@/components/interfaces/ProjectCreation/AdvancedConfiguration'
  18. import { ComputeSizeSelector } from '@/components/interfaces/ProjectCreation/ComputeSizeSelector'
  19. import { DatabasePasswordInput } from '@/components/interfaces/ProjectCreation/DatabasePasswordInput'
  20. import { DisabledWarningDueToIncident } from '@/components/interfaces/ProjectCreation/DisabledWarningDueToIncident'
  21. import { FreeProjectLimitWarning } from '@/components/interfaces/ProjectCreation/FreeProjectLimitWarning'
  22. import { InternalOnlyConfiguration } from '@/components/interfaces/ProjectCreation/InternalOnlyConfiguration'
  23. import { OrganizationSelector } from '@/components/interfaces/ProjectCreation/OrganizationSelector'
  24. import { extractPostgresVersionDetails } from '@/components/interfaces/ProjectCreation/PostgresVersionSelector'
  25. import { sizes } from '@/components/interfaces/ProjectCreation/ProjectCreation.constants'
  26. import { FormSchema } from '@/components/interfaces/ProjectCreation/ProjectCreation.schema'
  27. import {
  28. instanceLabel,
  29. smartRegionToExactRegion,
  30. } from '@/components/interfaces/ProjectCreation/ProjectCreation.utils'
  31. import { ProjectCreationFooter } from '@/components/interfaces/ProjectCreation/ProjectCreationFooter'
  32. import { ProjectNameInput } from '@/components/interfaces/ProjectCreation/ProjectNameInput'
  33. import { RegionSelector } from '@/components/interfaces/ProjectCreation/RegionSelector'
  34. import { SecurityOptions } from '@/components/interfaces/ProjectCreation/SecurityOptions'
  35. import {
  36. GitHubRepositoryField,
  37. useGitHubRepositoryOptions,
  38. } from '@/components/interfaces/Settings/Integrations/GithubIntegration/GitHubRepositoryField'
  39. import DefaultLayout from '@/components/layouts/DefaultLayout'
  40. import { WizardLayoutWithoutAuth } from '@/components/layouts/WizardLayout'
  41. import Panel from '@/components/ui/Panel'
  42. import { useAvailableOrioleImageVersion } from '@/data/config/project-creation-postgres-versions-query'
  43. import { useOverdueInvoicesQuery } from '@/data/invoices/invoices-overdue-query'
  44. import { useDefaultRegionQuery } from '@/data/misc/get-default-region-query'
  45. import { useAuthorizedAppsQuery } from '@/data/oauth/authorized-apps-query'
  46. import { useFreeProjectLimitCheckQuery } from '@/data/organizations/free-project-limit-check-query'
  47. import { useOrganizationAvailableRegionsQuery } from '@/data/organizations/organization-available-regions-query'
  48. import { useOrganizationsQuery } from '@/data/organizations/organizations-query'
  49. import { DesiredInstanceSize, instanceSizeSpecs } from '@/data/projects/new-project.constants'
  50. import {
  51. OrgProject,
  52. useOrgProjectsInfiniteQuery,
  53. } from '@/data/projects/org-projects-infinite-query'
  54. import {
  55. ProjectCreateVariables,
  56. useProjectCreateMutation,
  57. } from '@/data/projects/project-create-mutation'
  58. import { useCustomContent } from '@/hooks/custom-content/useCustomContent'
  59. import { useCheckEntitlements } from '@/hooks/misc/useCheckEntitlements'
  60. import { useAsyncCheckPermissions } from '@/hooks/misc/useCheckPermissions'
  61. import { useDataApiRevokeOnCreateDefaultEnabled } from '@/hooks/misc/useDataApiRevokeOnCreateDefault'
  62. import { useIsFeatureEnabled } from '@/hooks/misc/useIsFeatureEnabled'
  63. import { useLocalStorageQuery } from '@/hooks/misc/useLocalStorage'
  64. import { useSelectedOrganizationQuery } from '@/hooks/misc/useSelectedOrganization'
  65. import { withAuth } from '@/hooks/misc/withAuth'
  66. import { usePHFlag } from '@/hooks/ui/useFlag'
  67. import { DOCS_URL, PROJECT_STATUS, PROVIDERS, useDefaultProvider } from '@/lib/constants'
  68. import { buildStudioPageTitle } from '@/lib/page-title'
  69. import { useProfile } from '@/lib/profile'
  70. import { useTrack } from '@/lib/telemetry/track'
  71. import type { NextPageWithLayout } from '@/types'
  72. const sizesWithNoCostConfirmationRequired: DesiredInstanceSize[] = ['micro', 'small']
  73. const Wizard: NextPageWithLayout = () => {
  74. const track = useTrack()
  75. const router = useRouter()
  76. const { slug, projectName } = useParams()
  77. const { appTitle } = useCustomContent(['app:title'])
  78. const defaultProvider = useDefaultProvider()
  79. const { profile } = useProfile()
  80. const pageTitle = buildStudioPageTitle({
  81. section: 'New Project',
  82. brand: appTitle || 'Briven',
  83. })
  84. const { data: currentOrg } = useSelectedOrganizationQuery()
  85. const isFreePlan = currentOrg?.plan?.id === 'free'
  86. const canChooseInstanceSize = !isFreePlan
  87. const [lastVisitedOrganization] = useLocalStorageQuery(
  88. LOCAL_STORAGE_KEYS.LAST_VISITED_ORGANIZATION,
  89. ''
  90. )
  91. const { can: isAdmin } = useAsyncCheckPermissions(PermissionAction.CREATE, 'projects')
  92. const { can: canCreateGitHubConnection } = useAsyncCheckPermissions(
  93. PermissionAction.CREATE,
  94. 'integrations.github_connections'
  95. )
  96. const showAdvancedConfig = useIsFeatureEnabled('project_creation:show_advanced_config')
  97. const { hasAccess: hasAccessToGitHubIntegration } = useCheckEntitlements(
  98. 'integrations.github_connections'
  99. )
  100. const smartRegionEnabled = useFlag('enableSmartRegion')
  101. const projectCreationDisabled = useFlag('disableProjectCreationAndUpdate')
  102. const showInternalOnlyConfiguration = useFlag('newProjectInternalOnlyConfiguration')
  103. // Read the raw flag for telemetry — coerce-undefined-to-false would record false for
  104. // users whose flags haven't loaded yet. The raw value preserves undefined (omitted from
  105. // PostHog) so we only record true/false when the flag is resolved.
  106. const dataApiRevokeOnCreateDefaultFlag = usePHFlag<boolean>('dataApiRevokeOnCreateDefault')
  107. const isDataApiRevokeOnCreateDefault = useDataApiRevokeOnCreateDefaultEnabled()
  108. const isNotOnHigherPlan = !['team', 'enterprise', 'platform'].includes(currentOrg?.plan.id ?? '')
  109. // This is to make the database.new redirect work correctly. The database.new redirect should be set to supabase.com/dashboard/new/last-visited-org
  110. if (slug === 'last-visited-org') {
  111. if (lastVisitedOrganization) {
  112. router.replace(`/new/${lastVisitedOrganization}`, undefined, { shallow: true })
  113. } else {
  114. router.replace(`/new/_`, undefined, { shallow: true })
  115. }
  116. }
  117. const [allProjects, setAllProjects] = useState<OrgProject[] | undefined>(undefined)
  118. const [isComputeCostsConfirmationModalVisible, setIsComputeCostsConfirmationModalVisible] =
  119. useState(false)
  120. const form = useForm<z.infer<typeof FormSchema>>({
  121. resolver: zodResolver(FormSchema as any),
  122. mode: 'onChange',
  123. defaultValues: {
  124. organization: slug,
  125. projectName: projectName || '',
  126. highAvailability: false,
  127. postgresVersion: '',
  128. instanceType: '',
  129. cloudProvider: PROVIDERS[defaultProvider].id,
  130. dbPass: '',
  131. dbPassStrength: 0,
  132. dbPassStrengthMessage: '',
  133. dbRegion: undefined,
  134. githubRepositoryId: '',
  135. githubInstallationId: undefined,
  136. githubRepositoryName: '',
  137. instanceSize: canChooseInstanceSize ? sizes[0] : undefined,
  138. dataApi: true,
  139. dataApiDefaultPrivileges: !isDataApiRevokeOnCreateDefault,
  140. enableRlsEventTrigger: false,
  141. postgresVersionSelection: '',
  142. useOrioleDb: false,
  143. },
  144. })
  145. const { getFieldState, resetField, setValue } = form
  146. const {
  147. instanceSize: watchedInstanceSize,
  148. cloudProvider,
  149. dbRegion,
  150. githubRepositoryName,
  151. organization,
  152. projectName: watchedProjectName,
  153. highAvailability,
  154. } = useWatch({ control: form.control })
  155. // Read dirty state during render rather than depending on form.formState in the
  156. // effect — form.formState is a Proxy that gets a new reference every render, which
  157. // would re-fire this effect after each setValue and trigger an infinite loop.
  158. const isDataApiDefaultPrivilegesDirty = getFieldState(
  159. 'dataApiDefaultPrivileges',
  160. form.formState
  161. ).isDirty
  162. useEffect(() => {
  163. if (dataApiRevokeOnCreateDefaultFlag === undefined) return
  164. if (isDataApiDefaultPrivilegesDirty) return
  165. setValue('dataApiDefaultPrivileges', !dataApiRevokeOnCreateDefaultFlag, {
  166. shouldDirty: false,
  167. })
  168. }, [dataApiRevokeOnCreateDefaultFlag, isDataApiDefaultPrivilegesDirty, setValue])
  169. // [Charis] Since the form is updated in a useEffect, there is an edge case
  170. // when switching from free to paid, where canChooseInstanceSize is true for
  171. // an in-between render, but watchedInstanceSize is still undefined from the
  172. // form state carried over from the free plan. To avoid this, we set a
  173. // default instance size in this case.
  174. const instanceSize = canChooseInstanceSize ? (watchedInstanceSize ?? sizes[0]) : undefined
  175. const { data: membersExceededLimit = [] } = useFreeProjectLimitCheckQuery(
  176. { slug },
  177. { enabled: isFreePlan }
  178. )
  179. const hasMembersExceedingFreeTierLimit = membersExceededLimit.length > 0
  180. const freePlanWithExceedingLimits = isFreePlan && hasMembersExceedingFreeTierLimit
  181. const { data: organizations = [], isSuccess: isOrganizationsSuccess } = useOrganizationsQuery()
  182. const isEmptyOrganizations = isOrganizationsSuccess && organizations.length <= 0
  183. const { data: approvedOAuthApps = [] } = useAuthorizedAppsQuery(
  184. { slug },
  185. { enabled: !isFreePlan && slug !== '_' }
  186. )
  187. const hasOAuthApps = approvedOAuthApps.length > 0
  188. const { data: allOverdueInvoices = [] } = useOverdueInvoicesQuery({
  189. enabled: isNotOnHigherPlan,
  190. })
  191. const overdueInvoices = allOverdueInvoices.filter((x) => x.organization_id === currentOrg?.id)
  192. const hasOutstandingInvoices = isNotOnHigherPlan && overdueInvoices.length > 0
  193. const { data: orgProjectsFromApi } = useOrgProjectsInfiniteQuery({ slug: currentOrg?.slug })
  194. const allOrgProjects = useMemo(
  195. () => orgProjectsFromApi?.pages.flatMap((page) => page.projects),
  196. [orgProjectsFromApi?.pages]
  197. )
  198. const organizationProjects =
  199. allProjects?.filter((project) => project.status !== PROJECT_STATUS.INACTIVE) ?? []
  200. const availableComputeCredits = organizationProjects.length === 0 ? 10 : 0
  201. const additionalMonthlySpend = isFreePlan
  202. ? 0
  203. : instanceSizeSpecs[instanceSize as DesiredInstanceSize]!.priceMonthly - availableComputeCredits
  204. const { data: _defaultRegion, error: defaultRegionError } = useDefaultRegionQuery(
  205. {
  206. cloudProvider: PROVIDERS[defaultProvider].id,
  207. },
  208. {
  209. enabled: !smartRegionEnabled,
  210. refetchOnMount: false,
  211. refetchOnWindowFocus: false,
  212. refetchInterval: false,
  213. refetchOnReconnect: false,
  214. retry: false,
  215. }
  216. )
  217. const { data: availableRegionsData, error: availableRegionsError } =
  218. useOrganizationAvailableRegionsQuery(
  219. {
  220. slug: slug,
  221. cloudProvider: PROVIDERS[cloudProvider as CloudProvider].id,
  222. desiredInstanceSize: instanceSize as DesiredInstanceSize,
  223. },
  224. {
  225. enabled: smartRegionEnabled,
  226. refetchOnMount: false,
  227. refetchOnWindowFocus: false,
  228. refetchInterval: false,
  229. refetchOnReconnect: false,
  230. }
  231. )
  232. const recommendedSmartRegion = smartRegionEnabled
  233. ? availableRegionsData?.recommendations.smartGroup.name
  234. : ''
  235. const regionError =
  236. smartRegionEnabled && defaultProvider !== 'AWS_NIMBUS'
  237. ? availableRegionsError
  238. : defaultRegionError
  239. const defaultRegion =
  240. defaultProvider === 'AWS_NIMBUS'
  241. ? AWS_REGIONS.EAST_US.displayName
  242. : smartRegionEnabled
  243. ? availableRegionsData?.recommendations.smartGroup.name
  244. : _defaultRegion
  245. const canCreateProject = isAdmin && !freePlanWithExceedingLimits && !hasOutstandingInvoices
  246. const canConfigureGitHubOnCreate =
  247. canCreateProject && hasAccessToGitHubIntegration && canCreateGitHubConnection
  248. const dbRegionExact = smartRegionToExactRegion(dbRegion ?? '')
  249. const availableOrioleVersion = useAvailableOrioleImageVersion(
  250. {
  251. cloudProvider: cloudProvider as CloudProvider,
  252. dbRegion: smartRegionEnabled ? dbRegionExact : (dbRegion ?? ''),
  253. organizationSlug: organization,
  254. },
  255. { enabled: currentOrg !== null }
  256. )
  257. const userPrimaryEmail = profile?.primary_email?.toLowerCase()
  258. const isUserAtFreeProjectLimit = userPrimaryEmail
  259. ? membersExceededLimit.some(
  260. (member) => member.primary_email?.toLowerCase() === userPrimaryEmail
  261. )
  262. : false
  263. const shouldShowFreeProjectInfo = !!currentOrg && !isFreePlan && !isUserAtFreeProjectLimit
  264. const {
  265. gitHubAuthorization,
  266. githubRepos,
  267. hasPartialResponseDueToSSO,
  268. isLoading: isLoadingRepositoryOptions,
  269. refetch: refetchRepositoryOptions,
  270. } = useGitHubRepositoryOptions()
  271. const {
  272. mutate: createProject,
  273. isPending: isCreatingNewProject,
  274. isSuccess: isSuccessNewProject,
  275. } = useProjectCreateMutation({
  276. onSuccess: (res) => {
  277. track(
  278. 'project_creation_simple_version_submitted',
  279. {
  280. surface: 'main',
  281. instanceSize: form.getValues('instanceSize'),
  282. enableRlsEventTrigger: form.getValues('enableRlsEventTrigger'),
  283. dataApiEnabled: form.getValues('dataApi'),
  284. dataApiDefaultPrivilegesGranted: form.getValues('dataApiDefaultPrivileges'),
  285. useOrioleDb: form.getValues('useOrioleDb'),
  286. ...(dataApiRevokeOnCreateDefaultFlag !== undefined && {
  287. dataApiRevokeOnCreateDefaultEnabled: dataApiRevokeOnCreateDefaultFlag,
  288. }),
  289. },
  290. {
  291. project: res.ref,
  292. organization: res.organization_slug,
  293. }
  294. )
  295. router.push(`/project/${res.ref}`)
  296. },
  297. })
  298. const onSubmitWithComputeCostsConfirmation = async (values: z.infer<typeof FormSchema>) => {
  299. const launchingLargerInstance =
  300. values.instanceSize &&
  301. !sizesWithNoCostConfirmationRequired.includes(values.instanceSize as DesiredInstanceSize)
  302. if (additionalMonthlySpend > 0 && (hasOAuthApps || launchingLargerInstance)) {
  303. track('project_creation_simple_version_confirm_modal_opened', {
  304. instanceSize: values.instanceSize,
  305. })
  306. setIsComputeCostsConfirmationModalVisible(true)
  307. } else {
  308. await onSubmit(values)
  309. }
  310. }
  311. const onSubmit = async (values: z.infer<typeof FormSchema>) => {
  312. if (!currentOrg) return console.error('Unable to retrieve current organization')
  313. const {
  314. cloudProvider,
  315. projectName,
  316. highAvailability,
  317. dbPass,
  318. dbRegion,
  319. postgresVersion,
  320. instanceType,
  321. instanceSize,
  322. dataApi,
  323. dataApiDefaultPrivileges,
  324. enableRlsEventTrigger,
  325. postgresVersionSelection,
  326. useOrioleDb,
  327. githubInstallationId,
  328. githubRepositoryId,
  329. } = values
  330. if (useOrioleDb && !availableOrioleVersion) {
  331. return toast.error('No available OrioleDB image found, only Postgres is available')
  332. }
  333. const { postgresEngine, releaseChannel } =
  334. extractPostgresVersionDetails(postgresVersionSelection)
  335. const { smartGroup = [], specific = [] } = availableRegionsData?.all ?? {}
  336. const selectedRegion = smartRegionEnabled
  337. ? (smartGroup.find((x) => x.name === dbRegion) ?? specific.find((x) => x.name === dbRegion))
  338. : undefined
  339. const parsedGitHubRepositoryId =
  340. githubRepositoryId.length > 0 ? Number(githubRepositoryId) : undefined
  341. const shouldIncludeGitHubFields =
  342. githubInstallationId !== undefined && Number.isFinite(parsedGitHubRepositoryId)
  343. const data: ProjectCreateVariables = {
  344. dbPass,
  345. cloudProvider,
  346. organizationSlug: currentOrg.slug,
  347. name: projectName,
  348. highAvailability,
  349. // gets ignored due to org billing subscription anyway
  350. dbPricingTierId: 'tier_free',
  351. // only set the compute size on pro+ plans. Free plans always use micro (nano in the future) size.
  352. dbInstanceSize: isFreePlan ? undefined : (instanceSize as DesiredInstanceSize),
  353. dataApiExposedSchemas: !dataApi ? [] : undefined,
  354. dataApiUseApiSchema: false,
  355. dataApiRevokeDefaultPrivileges: dataApi && !dataApiDefaultPrivileges,
  356. postgresEngine: useOrioleDb ? availableOrioleVersion?.postgres_engine : postgresEngine,
  357. releaseChannel: useOrioleDb ? availableOrioleVersion?.release_channel : releaseChannel,
  358. ...(smartRegionEnabled ? { regionSelection: selectedRegion } : { dbRegion }),
  359. dbSql: enableRlsEventTrigger ? AUTO_ENABLE_RLS_EVENT_TRIGGER_SQL : undefined,
  360. ...(shouldIncludeGitHubFields
  361. ? {
  362. githubInstallationId,
  363. githubRepositoryId: parsedGitHubRepositoryId,
  364. }
  365. : {}),
  366. }
  367. if (postgresVersion && !postgresVersion.match(/1[2-9]\..*/)) {
  368. toast.error(
  369. `Invalid Postgres version, should start with a number between 12-19, a dot and additional characters, i.e. 15.2 or 15.2.0-3`
  370. )
  371. }
  372. if (postgresVersion || instanceType) {
  373. data['customBrivenRequest'] = {
  374. ami: {
  375. ...(postgresVersion && {
  376. search_tags: { 'tag:postgresVersion': postgresVersion },
  377. }),
  378. ...(instanceType && { instance_type: instanceType }),
  379. },
  380. }
  381. }
  382. createProject(data)
  383. }
  384. useEffect(() => {
  385. // Only set once to ensure compute credits dont change while project is being created
  386. if (allOrgProjects && allOrgProjects.length > 0 && !allProjects) {
  387. setAllProjects(allOrgProjects)
  388. }
  389. }, [allOrgProjects, allProjects, setAllProjects])
  390. useEffect(() => {
  391. // Handle no org: redirect to new org route
  392. if (isEmptyOrganizations) {
  393. router.push(`/new`)
  394. }
  395. }, [isEmptyOrganizations, router])
  396. useEffect(() => {
  397. // [Joshen] Cause slug depends on router which doesnt load immediately on render
  398. // While the form data does load immediately
  399. if (slug && slug !== '_') setValue('organization', slug)
  400. if (projectName) setValue('projectName', projectName || '')
  401. }, [slug, setValue, projectName])
  402. const isDbRegionDirty = getFieldState('dbRegion', form.formState).isDirty
  403. useEffect(() => {
  404. if (!isDbRegionDirty && defaultRegion) {
  405. setValue('dbRegion', defaultRegion)
  406. }
  407. }, [defaultRegion, isDbRegionDirty, setValue])
  408. useEffect(() => {
  409. if (regionError) {
  410. resetField('dbRegion', {
  411. defaultValue: PROVIDERS[defaultProvider].default_region.displayName,
  412. })
  413. }
  414. }, [regionError, resetField, defaultProvider])
  415. useEffect(() => {
  416. if (!isDbRegionDirty && recommendedSmartRegion) {
  417. setValue('dbRegion', recommendedSmartRegion)
  418. }
  419. }, [recommendedSmartRegion, isDbRegionDirty, setValue])
  420. useEffect(() => {
  421. if (highAvailability && cloudProvider !== 'AWS_K8S') {
  422. setValue('cloudProvider', 'AWS_K8S')
  423. }
  424. }, [highAvailability, cloudProvider, setValue])
  425. useEffect(() => {
  426. if (watchedInstanceSize !== instanceSize) {
  427. setValue('instanceSize', instanceSize, {
  428. shouldDirty: false,
  429. shouldValidate: false,
  430. shouldTouch: false,
  431. })
  432. }
  433. }, [instanceSize, watchedInstanceSize, setValue])
  434. useEffect(() => {
  435. if (!githubRepositoryName) return
  436. if ((watchedProjectName ?? '').trim().length > 0) return
  437. const repoName = githubRepositoryName.split('/').at(-1) ?? githubRepositoryName
  438. setValue('projectName', repoName.trim(), {
  439. shouldValidate: true,
  440. })
  441. }, [githubRepositoryName, watchedProjectName, setValue])
  442. return (
  443. <>
  444. {/* Wizard layouts set the visual header but not the browser tab title. */}
  445. <Head>
  446. <title>{pageTitle}</title>
  447. <meta name="description" content="Briven Studio" />
  448. </Head>
  449. <Form {...form}>
  450. <form onSubmit={form.handleSubmit(onSubmitWithComputeCostsConfirmation)}>
  451. <Panel
  452. loading={!isOrganizationsSuccess}
  453. title={
  454. <div key="panel-title">
  455. <h3>Create a new project</h3>
  456. <p className="text-sm text-foreground-lighter text-balance">
  457. Your project will have its own dedicated instance and full Postgres database. An
  458. API will be set up so you can easily interact with your new database.
  459. </p>
  460. </div>
  461. }
  462. footer={
  463. <ProjectCreationFooter
  464. form={form}
  465. canCreateProject={canCreateProject}
  466. instanceSize={instanceSize}
  467. organizationProjects={organizationProjects}
  468. isCreatingNewProject={isCreatingNewProject}
  469. isSuccessNewProject={isSuccessNewProject}
  470. />
  471. }
  472. >
  473. <>
  474. {projectCreationDisabled ? (
  475. <DisabledWarningDueToIncident title="Project creation is currently disabled" />
  476. ) : (
  477. <div className="divide-y divide-border-border">
  478. <OrganizationSelector form={form} />
  479. {canCreateProject && (
  480. <>
  481. {canConfigureGitHubOnCreate && (
  482. <Panel.Content>
  483. <GitHubRepositoryField
  484. form={form}
  485. name="githubRepositoryId"
  486. installationIdField="githubInstallationId"
  487. repositoryNameField="githubRepositoryName"
  488. label="GitHub (optional)"
  489. description={
  490. <>
  491. Ideal for agent-first workflows: update your schema in code, push it
  492. to GitHub, and Briven deploys the changes automatically.{' '}
  493. <a
  494. href="https://supabase.com/docs/guides/deployment/branching/github-integration"
  495. target="_blank"
  496. rel="noreferrer noopener"
  497. className="text-link"
  498. >
  499. Learn more
  500. </a>
  501. </>
  502. }
  503. disabled={isCreatingNewProject}
  504. repositories={githubRepos}
  505. gitHubAuthorization={gitHubAuthorization}
  506. hasPartialResponseDueToSSO={hasPartialResponseDueToSSO}
  507. isLoading={isLoadingRepositoryOptions}
  508. refetch={refetchRepositoryOptions}
  509. onConnectClick={() => track('project_creation_github_connect_clicked')}
  510. />
  511. </Panel.Content>
  512. )}
  513. <ProjectNameInput form={form} />
  514. {canChooseInstanceSize && <ComputeSizeSelector form={form} />}
  515. <DatabasePasswordInput form={form} />
  516. <RegionSelector
  517. form={form}
  518. instanceSize={instanceSize as DesiredInstanceSize}
  519. />
  520. <SecurityOptions form={form} />
  521. {showInternalOnlyConfiguration && <InternalOnlyConfiguration form={form} />}
  522. {showAdvancedConfig && !!availableOrioleVersion && (
  523. <AdvancedConfiguration form={form} />
  524. )}
  525. {shouldShowFreeProjectInfo ? (
  526. <Admonition
  527. className="rounded-none border-0"
  528. type="note"
  529. title="Need a free project?"
  530. description={
  531. <p>
  532. You can have up to 2 free projects across all organizations.{' '}
  533. <Link className="underline text-foreground" href="/new">
  534. Create a free organization
  535. </Link>{' '}
  536. to use them.
  537. </p>
  538. }
  539. />
  540. ) : null}
  541. </>
  542. )}
  543. {freePlanWithExceedingLimits ? (
  544. isAdmin &&
  545. slug && (
  546. <FreeProjectLimitWarning membersExceededLimit={membersExceededLimit || []} />
  547. )
  548. ) : hasOutstandingInvoices ? (
  549. <Panel.Content>
  550. <Admonition
  551. type="default"
  552. title="Your organization has overdue invoices"
  553. description={
  554. <div className="space-y-3">
  555. <p className="text-sm leading-normal">
  556. Please resolve all outstanding invoices first before creating a new
  557. project
  558. </p>
  559. <div>
  560. <Button asChild type="default">
  561. <Link href={`/org/${slug}/billing#invoices`}>View invoices</Link>
  562. </Button>
  563. </div>
  564. </div>
  565. }
  566. />
  567. </Panel.Content>
  568. ) : null}
  569. </div>
  570. )}
  571. </>
  572. </Panel>
  573. <ConfirmationModal
  574. size="large"
  575. loading={false}
  576. visible={isComputeCostsConfirmationModalVisible}
  577. title="Confirm compute costs"
  578. confirmLabel="I understand"
  579. onCancel={() => setIsComputeCostsConfirmationModalVisible(false)}
  580. onConfirm={async () => {
  581. const values = form.getValues()
  582. await onSubmit(values)
  583. setIsComputeCostsConfirmationModalVisible(false)
  584. }}
  585. variant={'warning'}
  586. >
  587. <div className="text-sm text-foreground-light space-y-1">
  588. <p>
  589. Launching a project on compute size "{instanceLabel(instanceSize)}" increases your
  590. monthly costs by ${additionalMonthlySpend}, independent of how actively you use it.
  591. By clicking "I understand", you agree to the additional costs.{' '}
  592. <Link
  593. href={`${DOCS_URL}/guides/platform/manage-your-usage/compute`}
  594. target="_blank"
  595. className="underline"
  596. >
  597. Compute Costs
  598. </Link>{' '}
  599. are non-refundable.
  600. </p>
  601. </div>
  602. </ConfirmationModal>
  603. </form>
  604. </Form>
  605. </>
  606. )
  607. }
  608. const PageLayout = withAuth(({ children }: PropsWithChildren) => {
  609. return <WizardLayoutWithoutAuth>{children}</WizardLayoutWithoutAuth>
  610. })
  611. Wizard.getLayout = (page) => (
  612. <DefaultLayout hideMobileMenu headerTitle="New project">
  613. <PageLayout>{page}</PageLayout>
  614. </DefaultLayout>
  615. )
  616. export default Wizard